单元测试基础:测试分层、环境搭建与 core 层测试
组件库没有测试等于没有质量保障。AI 时代写测试的门槛大幅降低,但"测什么、不测什么"的判断反而更重要。这节先建立测试分层的概念,让 AI 把测试环境搭起来,再给 core 包写单元测试。
单元测试与集成测试:分界在哪
测试分两大类,边界按"被测对象的大小"划分:
- 单元测试:覆盖工具方法、单个组件、小的功能模块的正常、边界、异常三种情况。落到组件库场景:消息是否正常发送、思考过程是否正常响应、消息样式左右侧(用户/助手)渲染是否正确、控制台有没有报错、附件能否正常发送,这是正常情况;用户发 6 个附件但组件上限是 5 个,应该报错,这是异常情况;按钮被快速重复点击时加载状态是否正确、请求有没有防抖,这是边界情况。
- 集成测试(端到端测试):模拟用户从浏览器、移动端真实访问应用的完整流程——输入用户名密码登录、点选订单里的商品下单,验证的是"用户能否完成任务",而不是某个函数的返回值。
两类测试不需要全覆盖。判断标准:
| 情况 | 做法 |
|---|---|
| 工具方法、单个组件、小功能模块 | 单元测试 |
| 核心流程(登录、下单、发送消息全链路) | 集成测试 |
| 频繁变化的部分 | 少测或不测——AI 时代重构更频繁,给天天要改的代码写满测试,维护成本反而高于收益 |
AI 时代测试的价值重估
传统开发里写测试是程序员最头疼的事之一,现在这件事被 AI 基本解决了:
- 写测试不再是问题:主流模型写组件测试用例是"小儿科"级别,AI 会主动扫描组件有哪些属性项、你提出的测试需求有哪些,自动生成正常/异常/边界三类用例。
- 覆盖率不再是问题:全量补测一次就能把行覆盖从 87% 拉到 98%(本仓库 2026-08 实测)。
- 唯一剩下的问题:AI 会不会监守自盗——它既是运动员(写实现)又是裁判员(写测试)。这是第 02 节的主题,这里先记住结论:绝大多数组件和单元模块的测试生成没有问题,风险集中在断言与实现同源同错,需要规则约束。
让 AI 写测试之前自己要先想清楚三件事(这也是写提示词的骨架):测什么(目标)、用什么测(工具选型)、测到什么程度算完(覆盖标准)。至于环境准备、依赖集成、自动化脚本配置,都可以交给 AI 完成。
向成熟开源项目看测试长什么样
不用凭空想象测试该怎么组织。GitHub 上找几个著名组件库(可以让模型推荐),看它们的测试目录是标准学习方法。以 Element Plus 为例:进入仓库 packages 目录,任选一个组件,里面都有 test 目录,文件结构有三段固定写法:
- 测试前的准备:先准备环境或数据
- 测试场景的描述:渲染测试、
offset偏移量属性测试、定位(placement)属性测试——把组件的每个基础用法和属性项的正常情况全部测到 - 测试后的恢复:清理数据、还原状态
值得注意的是 Element Plus 的测试框架用的是 Vitest——早年 Vue 生态用 Jest 的多,现在头部组件库基本都迁到了 Vitest。原因很直接:效率高、与 Vite 深度绑定,一套 Vite 配置文件同时驱动 dev 和 test,不需要为测试单独维护一份 webpack/jest 配置。
测试工具链与版本现状(2026-11-07 核实)
| 工具 | 角色 | 最新版本 | 本仓库锁定 |
|---|---|---|---|
| Vitest | 测试框架与 runner | 5.1.0(2026-11-07,npm latest) | ^4.1.10 |
| @vue/test-utils | 组件测试 | 2.5.1(2026-09-16) | ^2.4.11 |
| @testing-library/vue | 集成测试(宿主视角) | 8.x | ^8.1.0 |
| @testing-library/jest-dom | DOM 断言 matchers | 7.x | ^7.0.1 |
| @testing-library/user-event | 真实键盘鼠标模拟 | 14.x | ^14.6.5 |
| jsdom | 浏览器 DOM 模拟 | 31.x | ^30.0.1 |
| @vitest/coverage-v8 | 覆盖率引擎 | 5.1.0(与 Vitest 同版本联动) | ^4.1.10 |
| @playwright/test | E2E | 1.65.0(1.63 起 Chromium 153 / Firefox 155 / WebKit 26.6) | ^1.62.1 |
版本节奏说明(来源:npm registry 与 Vitest/Playwright 官方发布记录):Vitest 4.0 于 2025-10-22 发布(Browser Mode 转正),4.1 于 2026-03-12 发布,5.0 于 2026-09-03 发布(vm pools 提速最高 53%、内置 Trace View 与 vitest doctor),V4 分支仍在维护(4.1.11,2026-08-18)。本项目按"不主动追最新"原则停在 4.1.x,升级 5.x 时注意其 breaking changes(如 loupe.inspect 换 pretty-format)。
用一条指令让 AI 搭好测试环境
环境搭建完全可以让 AI 代劳。真实使用的提示词:
在我们的项目里集成单元测试和集成测试的技术环境:
- 使用现在主流的测试工具包
- 加入一个最基础的测试示例
- 主要针对我们的组件库项目
- 在文档部分指南里面新增一个新的分组「开发与测试」,添加测试相关的说明
text
AI 判断任务复杂后会先 brainstorm 给出方案。实测给出的选项:
- 方案 A(采纳):Vitest + Testing Library Vue 三件套(
@testing-library/vue/jest-dom/user-event)渐进引入,新增集成测试和基础示例,用 Testing Library 风格写 - 备选:继续只用 @vue/test-utils(存量组件测试)
- 另有 Playwright 真浏览器 E2E 一档,作为独立层后续单独引入(第 04 节)
几分钟集成完成后,仓库里多出三样东西:测试依赖与配置、一个可直接运行的基础测试示例、文档站"开发与测试"分组下的测试指南(测试文件创建规则、工具链说明——后续 AI 会按这份文档的约定继续写测试)。
真实仓库的测试配置
ai-chat-ui 的测试配置集中在根目录一份 vitest.config.ts,所有包共用(早期是每包一份配置,重构后收敛为根级单配置,避免 N 份配置漂移):
// vitest.config.ts(仓库根目录,节选)
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
// 包 exports 指向 dist 产物,测试内的值导入统一走源码解析
'@toimc/core': fileURLToPath(new URL('./packages/core/src/index.ts', import.meta.url)),
// 子路径须列在主入口之前(alias 前缀匹配,先到先得)
'@toimc/agents/mastra': fileURLToPath(new URL('./packages/agents/src/mastra/index.ts', import.meta.url)),
'@toimc/agents': fileURLToPath(new URL('./packages/agents/src/index.ts', import.meta.url)),
'@toimc/server/mastra': fileURLToPath(new URL('./packages/server/src/mastra/index.ts', import.meta.url)),
'@toimc/server': fileURLToPath(new URL('./packages/server/src/index.ts', import.meta.url)),
},
},
test: {
globals: true, // describe/it/expect 无需 import
environment: 'jsdom',
include: ['packages/*/__tests__/**/*.{test,spec}.{ts,tsx}'],
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
include: ['packages/markdown/src/**'],
exclude: ['packages/markdown/src/types/**'],
thresholds: { statements: 80, branches: 75, functions: 80, lines: 80 },
},
},
})
typescript
三个设计点:
- alias 指向源码:包的
exports字段指向dist/构建产物,测试如果走 exports 就在测旧产物;alias 强制测试内的值导入解析到src/源码,测试永远测当前代码。 - 测试文件位置强制:
include只认packages/*/__tests__/,目录结构镜像src/(src/composables/foo.ts↔__tests__/composables/foo.test.ts),禁止测试与源码混放。早期测试文件直接建在组件目录旁边,组件一多就难管理,这是后来用规则(.claude/rules/project-structure.md)固化的演进。 - 根级
tests/setup.ts全局 setup:注册 jest-dom matchers + polyfill jsdom 缺失的 API。
// tests/setup.ts(仓库根目录)
import '@testing-library/jest-dom/vitest'
import { installSpeechMocks } from './mocks/speech'
installSpeechMocks()
if (typeof window !== 'undefined' && !window.matchMedia) {
window.matchMedia = (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
})
}
if (typeof window !== 'undefined' && typeof window.ResizeObserver === 'undefined') {
window.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
}
}
typescript
matchMedia(暗色模式监听)和 ResizeObserver(容器查询)是 jsdom 缺失的高频 API,在 setup 统一 polyfill,不在单个测试里打补丁。
core 层测试策略
core 包是纯逻辑层,没有 DOM,environment: 'jsdom' 下照常跑。按模块分文件,测试放 packages/core/__tests__/(镜像 src)。
工具函数测试
// packages/core/__tests__/utils/index.test.ts(真实用例)
import { describe, it, expect } from 'vitest'
import { generateId, createUserMessage, createAssistantMessage } from '../../src/utils'
describe('generateId', () => {
it('should generate unique ids', () => {
expect(generateId()).not.toBe(generateId())
})
it('should start with msg_ prefix', () => {
expect(generateId()).toMatch(/^msg_\d+_\d+$/)
})
})
describe('createUserMessage', () => {
it('should create a user message with content', () => {
const msg = createUserMessage('Hello')
expect(msg.role).toBe('user')
expect(msg.content).toBe('Hello')
expect(msg.id).toBeTruthy()
expect(msg.createdAt).toBeInstanceOf(Date)
})
it('should create a user message with attachments', () => {
const attachment = {
id: 'att_1',
url: 'https://example.com/img.png',
name: 'img.png',
mediaType: 'image/png',
}
const msg = createUserMessage('Look at this', [attachment])
expect(msg.attachments).toHaveLength(1)
expect(msg.attachments![0].mediaType).toBe('image/png')
})
})
typescript
useChat 状态机测试(AsyncGenerator 流消费)
// packages/core/__tests__/composables/index.test.ts(真实用例,节选)
import { describe, it, expect } from 'vitest'
import { useChat } from '../../src/composables'
import type { ChatAdapter, StreamChunk } from '../../src/types'
function createMockAdapter(chunks: StreamChunk[]): ChatAdapter {
return {
async *sendMessage() {
for (const chunk of chunks) {
yield chunk
}
},
}
}
describe('useChat', () => {
it('should send a message and receive streamed response', async () => {
const adapter = createMockAdapter([
{ type: 'text', content: 'Hello ' },
{ type: 'text', content: 'World' },
{ type: 'done', content: '' },
])
const state = useChat(adapter)
await state.send('Hi')
expect(state.messages).toHaveLength(2)
expect(state.messages[0].role).toBe('user')
expect(state.messages[0].content).toBe('Hi')
expect(state.messages[1].content).toBe('Hello World')
expect(state.isStreaming).toBe(false)
})
})
typescript
用 async *function 构造 mock 流、for await 逐 chunk 断言,是测一切流式逻辑的通用模式。异常路径同样要有:adapter yield 出 { type: 'error' } chunk 时,断言状态进入错误态而非崩溃。
类型验证测试
Message 的角色联合、可选字段(attachments、metadata)用类型标注 + 运行时断言双重验证——给变量标注 Message 类型,TypeScript 编译通过即验证了类型契约,断言验证运行时行为。
用 AI 生成测试
基于 @toimc/core 包的所有导出函数和类型,生成完整的单元测试。
要求:
1. 使用 Vitest,describe/it/expect 风格
2. 每个函数至少 3 个测试用例:正常值、边界值、错误情况
3. 类型测试确保 TypeScript 编译通过
4. 覆盖率目标 80% 以上
text
向不同模型要"AI 写测试的操作建议"(如 DeepSeek),各家说法大同小异,归纳下来关键就一条:prompt 里把目标、范围、验收标准说清楚,让 AI 做"准备环境 → 集成配置 → 配好自动化脚本 → 读代码写测试"这一串事。AI 会读取源码分析每个函数的参数和返回值,自动生成三类场景用例;对 AsyncGenerator 这类复杂异步逻辑,也能正确处理 for await...of 的测试。
一个实战技巧:优先攻击从未测过的公开选项。useChat 的 maxHistory 与 abort 是文档里明写的 API,却长期没有用例——补上第一条 maxHistory 用例就直接暴露了一个启用即栈溢出的生产 bug。
小结
- 测试分单元(正常/边界/异常三场景,测函数与小组件)与集成(模拟真实用户旅程,只测核心流程)两层,频繁变化的部分不硬凑覆盖
- AI 时代写测试和覆盖率都不再是难题,唯一要防的是 AI 监守自盗(下一节主题)
- Vitest 是 Vite 生态原生测试框架,Element Plus 等头部组件库已从 Jest 迁入;2026-11 最新版 5.1.0,本仓库稳在 4.1.x
- 测试配置收敛为根级单
vitest.config.ts:alias 指向源码、__tests__/镜像 src、全局 setup polyfill jsdom 缺失 API - core 层纯逻辑最好测:工具函数断言前缀与唯一性、useChat 用 mock AsyncGenerator 流消费
下一节用 Vue Test Utils 与 Testing Library 测试有 DOM 的组件,并正面解决"AI 监守自盗"问题。
↑